import { WeiQi, WeiQiMusic, Chess, ErrorMsg, ErrorCode, ChessType, ClickBoardEvent } from './WeiQi'

function clone(o: ChessType[][]) {
  return JSON.parse(JSON.stringify(o))
}

export interface Options {
  boardSize?: number
}

interface Pos {
  x: number,
  y: number
}

interface DaJiesMap {
  [key: string]: {
    stepCount: number
    pos: Pos
  }
}

interface CheckedPosTable {
  [key: string]: boolean
}

enum DropResult {
  // 落子
  ORDINARY = '0',
  // 提子
  EAT = '1',
  // 失败
  FAIL = '2'
}

interface DropChessResult {
  result: DropResult,
  errorMsg?: ErrorMsg
}

class M {
  go() {}
  eat() {}
  release() {}
}

function getDivWidth(div: HTMLDivElement) {
  return div.offsetWidth || parseFloat(div.style.width) || 0
}

function getDivHeight(div: HTMLDivElement) {
  return div.offsetHeight || parseFloat(div.style.height) || 0
}

export default class WeiQiImpl implements WeiQi {
  private divDom: HTMLDivElement
  private canvas: HTMLCanvasElement
  private context: CanvasRenderingContext2D
  // 计步器
  private stepCount = 0
  // 打击标记 把打劫的放在里面
  private daJies: DaJiesMap = {}
  private stepList: Chess[] = []
  private chessBoard: ChessType[][]
  private music: WeiQiMusic = new M()
  private roadNum = 19
  private static defaultChessType = ChessType.BLACK
  private defaultChessType = WeiQiImpl.defaultChessType

  private beforePutCallBack: (chess: Chess) => boolean = () => true
  private errorCallBack: (err: ErrorMsg) => void = () => {}
  private afterPutCallBack: (chess: Chess) => void = () => {}

  private get gridWidth(): number {
    return this.width / (this.roadNum + 1)
  }

  private get width(): number {
    return Math.min(getDivWidth(this.divDom), getDivHeight(this.divDom))
  }

  public constructor(dom: HTMLDivElement, options: Options | null) {
    this.divDom = dom
    this.canvas = document.createElement('canvas')
    this.context = this.canvas.getContext('2d') as CanvasRenderingContext2D
    this.chessBoard = this.initChessBoard()
    this.initCanvas()
    this.drawBoard()
    this.drawStars()
    if (options?.boardSize) {
      this.boardSize = options?.boardSize
    }
  }

  private initCanvas() {
    this.canvas.width = this.width
    this.canvas.height = this.width
    this.canvas.style.background = '#a97652'
    this.canvas.style.cursor = 'pointer'
    this.divDom.appendChild(this.canvas)
    this.divDom.onclick = e => this.clickBoard(e)
  }

  public clickBoard(event: ClickBoardEvent, callBack?: (res: boolean) => void): void {
    const g = this.gridWidth
    const x = event.offsetX - g
    const y = event.offsetY - g
    const i = Math.round(x / g)
    const j = Math.round(y / g)
    // 默认黑行，如需改变需要用户控制
    const chessman = { x: i, y: j, type: this.defaultChessType };
    (async function (that) {
      const isDrop = await that.beforePutCallBack(chessman)
      let result = false
      if (isDrop !== false) {
        that.dropChess(chessman)
        result = true
        that.afterPutCallBack(chessman)
      }
      callBack && callBack(result)
    })(this)
  }

  public set chessType(chessType: ChessType) {
    this.defaultChessType = chessType
  }

  public get chessType(): ChessType {
    return this.defaultChessType
  }

  private drawBoard() {
    this.context.lineWidth = 1
    this.context.strokeStyle = '#000000'
    const g = this.gridWidth
    const w = this.width
    /*画网格*/
    for (let i = 0; i < this.roadNum; i++) {
      this.context.beginPath()
      this.context.strokeStyle = '#272822'
      this.context.moveTo(g + i * g + 0.5, g)
      this.context.lineTo(g + i * g + 0.5, w - g)
      this.context.stroke()
      this.context.moveTo(g, g + i * g + 0.5)
      this.context.lineTo(w - g, g + i * g + 0.5)
      this.context.stroke()
    }
  }

  private drawStars() {
    const c = (this.roadNum - 1) / 2
    const s = (this.roadNum / 4) > 3 ? 3 : 2
    const e = (this.roadNum - 1) - s
    const isBig = this.roadNum == 19
    this.star(s, s)
    isBig && this.star(s, c)
    this.star(s, e)
    isBig && this.star(c, s)
    this.star(c, c)
    isBig && this.star(c, e)
    this.star(e, s)
    isBig && this.star(e, c)
    this.star(e, e)
  }

  private initChessBoard(): ChessType[][] {
    // 存储落子情况的二维数组
    const chessBoard: ChessType[][] = []
    for (let i = 0; i < this.roadNum; i++) {
      chessBoard[i] = []
      for (let j = 0; j < this.roadNum; j++) {
        chessBoard[i][j] = ChessType.NULL
      }
    }
    return chessBoard
  }

  /**
   * 改变棋盘尺寸
   */
  private changeCanvasSize(): void {
    this.canvas.width = this.width
    this.canvas.height = this.width
  }

  // 提子
  private removeBlock(chessBoard: ChessType[][], pos: Pos, deadArr: Chess[], cPos: CheckedPosTable | null) {
    const checkedPos = cPos || {}
    const { x, y } = pos
    const type = chessBoard[x][y]
    const deads = deadArr || []
    // 已经数过了
    if (checkedPos[`${x},${y}`] == true) {
      return []
    }
    // 如果有棋子
    if (chessBoard[x][y] != ChessType.NULL) {
      deads.push({ x: x, y: y, type: type })
      chessBoard[x][y] = ChessType.NULL
    }
    checkedPos[`${x},${y}`] = true
    const lx = x - 1
    const rx = x + 1
    const uy = y - 1
    const dy = y + 1
    if (lx >= 0 && chessBoard[lx][y] == type) {
      this.removeBlock(chessBoard, { x: lx, y }, deads, checkedPos)
    }
    if (rx < this.roadNum && chessBoard[rx][y] == type) {
      this.removeBlock(chessBoard, { x: rx, y }, deads, checkedPos)
    }
    if (uy >= 0 && chessBoard[x][uy] == type) {
      this.removeBlock(chessBoard, { x, y: uy }, deads, checkedPos)
    }
    if (dy < this.roadNum && chessBoard[x][dy] == type) {
      this.removeBlock(chessBoard, { x, y: dy }, deads, checkedPos)
    }
    return deads
  }

  // 数气
  private getBreath(chessBoard: ChessType[][], pos: Pos, cPos: CheckedPosTable | null): number {
    let q = 0
    const { x, y } = pos
    const checkedPos = cPos || {}
    const type = chessBoard[x][y]
    // 已经数过了
    if (checkedPos[`${x},${y}`]) {
      return 0
    }
    checkedPos[`${x},${y}`] = true
    const lx = x - 1
    const rx = x + 1
    const uy = y - 1
    const dy = y + 1
    if (lx >= 0) {
      // 空位
      if (chessBoard[lx][y] == ChessType.NULL) {
        q = q + 1
        // 自己棋子
      } else if (chessBoard[lx][y] == type) {
        q = q + this.getBreath(chessBoard, { x: lx, y }, checkedPos)
      }
    }
    if (rx < this.roadNum) {
      if (chessBoard[rx][y] == ChessType.NULL) {
        q = q + 1
      } else if (chessBoard[rx][y] == type) {
        q = q + this.getBreath(chessBoard, { x: rx, y }, checkedPos)
      }
    }
    if (uy >= 0) {
      // 空位
      if (chessBoard[x][uy] == ChessType.NULL) {
        q = q + 1
        // 自己棋子
      } else if (chessBoard[x][uy] == type) {
        q = q + this.getBreath(chessBoard, { x, y: uy }, checkedPos)
      }
    }
    if (dy < this.roadNum) {
      // 空位
      if (chessBoard[x][dy] == ChessType.NULL) {
        q = q + 1
        // 自己棋子
      } else if (chessBoard[x][dy] == type) {
        q = q + this.getBreath(chessBoard, { x, y: dy }, checkedPos)
      }
    }
    return q
  }

  private drawAll(): void {
    // TODO
    this.clearChessBoard()
    this.drawBoard()
    this.drawStars()
    for (let i = 0; i < this.chessBoard.length; i++) {
      for (let j = 0; j < this.chessBoard[i].length; j++) {
        const type = this.chessBoard[i][j]
        this.drawOneStep({
          x: i,
          y: j,
          type
        })
      }
    }
  }

  private isHasChess(chessBoard: ChessType[][], pos: Pos) {
    return chessBoard[pos.x][pos.y] != ChessType.NULL
  }

  // 判断是否越界
  private isOutBoard(chessBoard: ChessType[][], pos: Pos): boolean {
    const length = chessBoard.length
    return (pos.x < 0 || pos.x > length || pos.y < 0 || pos.y > length)
  }

  // 是否可以下子
  private isCanDrop(chess: Chess): ErrorMsg {
    if (this.isOutBoard(this.chessBoard, chess)) {
      return {
        code: ErrorCode.OUT,
        msg: '不在棋盘范围内'
      }
    }
    // 如果有子则不能下
    if (this.isHasChess(this.chessBoard, chess)) {
      return {
        code: ErrorCode.HAS,
        msg: '已经落子，不能落了'
      }
    }
    if (this.isSuicide(chess)) {
      return {
        code: ErrorCode.SUICIDE,
        msg: '没气'
      }
    }
    if (this.isDaJie(chess)) {
      return {
        code: ErrorCode.DAJIE,
        msg: '打劫'
      }
    }
    return {
      code: ErrorCode.SUCCESS,
      msg: '下棋成功'
    }
  }

  // 沙箱棋盘运行下判断是否是自杀
  private isSuicide(chess: Chess): boolean {
    // 获取沙箱棋局
    const chessBoardSandboxie = clone(this.chessBoard)
    // 沙箱内下棋
    this.putChess(chessBoardSandboxie, chess)
    // 沙箱内看是否提子
    const deaths = this.eatChess(chessBoardSandboxie, chess)
    if (deaths.length > 0) {
      // 此时吃子，不算自杀
      return false
    }
    // 如果没有尺子，则沙箱内数气
    const qi = this.getBreath(chessBoardSandboxie, chess, null)
    // 如果气是0则为自杀行为
    return qi == 0
  }

  private isDaJie(chess: Chess) {
    if (this.daJies[chess.type]?.stepCount == (this.stepCount + 1) && this.daJies[chess.type]?.pos.x == chess.x && this.daJies[chess.type]?.pos.y == chess.y) {
      return true
    }
    return false
  }

  // 沙箱内判断是否标记打劫
  private isMarkDaJie(chess: Chess, deaths: Chess[]) {
    // 获取沙箱棋局
    const chessBoardSandboxie = clone(this.chessBoard)

    // 提子一个有可能打劫
    if (deaths.length == 1) {
      // 被吃的棋子再下回去试试，看看能不能提子
      const deathChess = deaths[0]
      this.putChess(chessBoardSandboxie, deathChess)
      const deaths1 = this.eatChess(chessBoardSandboxie, deathChess)

      // 如果也是提子一颗，并且是原来的子，那么就是打劫
      // TODO 相等判断
      if (deaths1.length == 1 && deaths1[0].x == chess.x && deaths1[0].y == chess.y && deaths1[0].type == chess.type) {
        return true
      }
    }
    return false
  }

  // 逻辑下棋
  private logicDropChess(chess: Chess): DropChessResult {
    // 已经落子，不能落了
    const canDropResult = this.isCanDrop(chess)
    if (canDropResult.code !== ErrorCode.SUCCESS) {
      return {
        result: DropResult.FAIL,
        errorMsg: canDropResult
      }
    }
    // 下棋。重要
    this.putChess(this.chessBoard, chess)
    // 计步器加一
    this.stepCount++
    const deaths = this.eatChess(this.chessBoard, chess)
    // 标记打劫
    this.markDaJie(chess, deaths)
    if (deaths.length > 0) {
      return {
        result: DropResult.EAT
      }
    }
    return {
      result: DropResult.ORDINARY
    }
  }

  private dropChess(chess: Chess, errCallBack?: (err: ErrorMsg) => void): void {
    const logicDropChessRes = this.logicDropChess(chess)
    if ([DropResult.ORDINARY, DropResult.EAT].includes(logicDropChessRes.result)) {
      this.stepList.push(chess)
      this.makeMusic(logicDropChessRes.result == DropResult.EAT)
      this.drawAll()
      this.drawActive(chess.x, chess.y)
    }
    if (logicDropChessRes.errorMsg) {
      errCallBack && errCallBack(logicDropChessRes.errorMsg)
      this.errorCallBack(logicDropChessRes.errorMsg)
    }
  }


  private makeMusic(isEat: boolean): void {
    if (isEat) {
      this.music.eat()
    } else {
      this.music.go()
    }
  }

  private markDaJie(chess: Chess, deaths: Chess[]) {
    const isMDJ = this.isMarkDaJie(chess, deaths)
    if (isMDJ) {
      const resType = this.getReverseType(chess.type)
      this.daJies[resType] = {
        stepCount: this.stepCount + 1,
        pos: { x: deaths[0].x, y: deaths[0].y }
      }
    }
  }

  // 获取反向颜色
  private getReverseType(type: ChessType): ChessType {
    let resType = type
    switch (type) {
      case ChessType.BLACK:
        resType = ChessType.WHITE
        break
      case ChessType.WHITE:
        resType = ChessType.BLACK
        break
      default:
        resType = type
    }
    return resType
  }

  // 下棋
  private putChess(chessBoard: ChessType[][], chess: Chess) {
    chessBoard[chess.x][chess.y] = chess.type
  }

  // 吃子
  private eatChess(chessBoard: ChessType[][], chess: Chess): Chess[] {
    const deaths: Chess[] = []
    const lx = chess.x - 1
    const rx = chess.x + 1
    const uy = chess.y - 1
    const dy = chess.y + 1
    if (lx >= 0) {
      if (![chess.type, ChessType.NULL].includes(chessBoard[lx][chess.y])) {
        const death = this.autoRemoveChess(chessBoard, { x: lx, y: chess.y })
        deaths.push(...death)
      }
    }
    if (rx < this.roadNum) {
      if (![chess.type, ChessType.NULL].includes(chessBoard[rx][chess.y])) {
        const death = this.autoRemoveChess(chessBoard, { x: rx, y: chess.y })
        deaths.push(...death)
      }
    }
    if (uy >= 0) {
      if (![chess.type, ChessType.NULL].includes(chessBoard[chess.x][uy])) {
        const death = this.autoRemoveChess(chessBoard, { x: chess.x, y: uy })
        deaths.push(...death)
      }
    }
    if (dy < this.roadNum) {
      if (![chess.type, ChessType.NULL].includes(chessBoard[chess.x][dy])) {
        const death = this.autoRemoveChess(chessBoard, { x: chess.x, y: dy })
        deaths.push(...death)
      }
    }
    return deaths
  }


  private autoRemoveChess(chessBoard: ChessType[][], pos: Pos) {
    // 0就是没气了
    const NULL_QI = 0
    let death: Chess[] = []
    const qi = this.getBreath(chessBoard, pos, null)
    if (qi == NULL_QI) {
      // 吃子
      death = this.removeBlock(chessBoard, pos, [], null)
    }
    return death
  }

  private clearChessBoard() {
    this.context.clearRect(0, 0, this.width, this.width)
  }

  /*画特殊星位*/
  private star(k: number, l: number) {
    const g = this.gridWidth
    this.context.beginPath()
    this.context.fillStyle = '#2C2924'
    this.context.arc(g + k * g, g + l * g, 4, 0, 2 * Math.PI)
    this.context.fill()
    this.context.closePath()
  }

  private drawActive(i: number, j: number) {
    const g = this.gridWidth
    this.context.beginPath()
    this.context.fillStyle = 'red'
    this.context.arc(g + i * g, g + j * g, g / 6, 0, 2 * Math.PI)
    this.context.fill()
    this.context.closePath()
  }

  /*画棋子*/
  private drawOneStep(chess: Chess) {
    if (chess.type == ChessType.NULL) {
      return
    }
    const g = this.gridWidth
    this.context.beginPath()
    const gColor = this.context.createRadialGradient(g + chess.x * g + 2, g + chess.y * g - 2, 13, g + chess.x * g + 2, g + chess.y * g - 2, 0)
    if (chess.type === ChessType.BLACK) {
      gColor.addColorStop(0, '#0a0a0a')
      gColor.addColorStop(1, '#636766')
    } else if (chess.type = ChessType.WHITE) {
      gColor.addColorStop(0, '#d1d1d1')
      gColor.addColorStop(1, '#f9f9f9')
    }
    this.context.fillStyle = gColor
    this.context.arc(g + chess.x * g, g + chess.y * g, g / 2 - 1, 0, 2 * Math.PI)
    this.context.fill()
    this.context.closePath()
  }

  public set onBeforePut(callBack: (chess: Chess) => boolean) {
    this.beforePutCallBack = callBack
  }

  public set onAfterPut(callBack: (chess: Chess) => void) {
    this.afterPutCallBack = callBack
  }

  public set onError(callBack: (err: ErrorMsg) => void) {
    this.errorCallBack = callBack
  }

  /**
   * 设置棋盘尺寸
   */
  public set boardSize(size: number) {
    this.divDom.style.width = `${size}px`
    this.divDom.style.height = `${size}px`
    this.initSize()
  }

  public getChessBoard(): ChessType[][] {
    return this.chessBoard
  }

  // 读取每一步
  public readStep(x: number, y: number, type: ChessType, errCallBack?: (err: ErrorMsg) => void): void {
    this.dropChess({ x, y, type }, errCallBack)
  }

  // 悔棋一步
  // TODO 悔棋多步
  public backStep(): void {
    this.chessBoard = this.initChessBoard()
    this.stepList.pop()
    this.stepList.forEach(step => {
      this.logicDropChess(step)
    })
    this.drawAll()
  }

  public reset(): void {
    this.music.release()
    this.chessBoard = this.initChessBoard()
    this.stepCount = 0
    this.daJies = {}
    this.stepList = []
    this.beforePutCallBack = () => true
    this.errorCallBack = () => {}
    this.afterPutCallBack = () => {}
    this.defaultChessType = WeiQiImpl.defaultChessType
    this.drawAll()
  }

  public getStepCount(): number {
    return this.stepCount
  }

  public getSteps(): Chess[] {
    return this.stepList
  }

  public initSize(): void {
    this.changeCanvasSize()
    this.drawAll()
  }

  public changeRoadNum(n: number): void {
    this.roadNum = n / 1
    this.chessBoard = this.initChessBoard()
    this.changeCanvasSize()
    this.drawAll()
  }

  public useMusic(music: WeiQiMusic):void  {
    this.music = music
  }
}